package com.googlecode.mailnet.math.distribution;

import static com.googlecode.mailnet.math.distribution.CompositeDistribution.Operation.*;

import java.util.ArrayList;

public class DistributionParser {

	/**
	 * Creates the distribution for the given string expression.
     *
     * Look <a href="http://code.google.com/p/mailnet/wiki/DistributionStringFormat">here</a> for description
     * on supported expression format.
     *
	 * @param expression The string to build expression for.
	 * @throws DistributionFormatException if distribution expression is invalid
	 * @return Distribution object.
	 */
	public static Distribution create(String expression) throws DistributionFormatException {
        ParsingState state = new ParsingState(expression);
        Distribution result = parseExpression(state);
        if (state.pos != expression.length()) {
            raiseException(state, "extra characters remain after expression");
        }
		return result;
	}

    // ==================== Implementation ====================

    private static Distribution parseExpression(ParsingState state) throws DistributionFormatException {
        Distribution result = parseSummand(state);
        while (true) {
            char c = state.curChar();
            if (c != '+' && c != '-') {
                break;
            }
            state.pos++;
            result = new CompositeDistribution(result, parseSummand(state), c == '+' ? PLUS : MINUS);
        }
        return result;
    }

    private static Distribution parseSummand(ParsingState state) throws DistributionFormatException {
        Distribution result = parseMultiplier(state);
        while (true) {
            char c = state.curChar();
            if (c != '*' && c != '/') {
                break;
            }
            state.pos++;
            result = new CompositeDistribution(result, parseMultiplier(state), c == '*' ? MULTIPLY : DIVIDE);
        }
        return result;
    }

    private static Distribution parseMultiplier(ParsingState state) throws DistributionFormatException {
        char c = state.curChar();
        if (c == '-') { // unary minus
            state.pos++;
            Distribution d = parseMultiplier(state);
            if (d instanceof ConstantDistribution) {
                return new ConstantDistribution(-d.getValue());
            } else {
                return new CompositeDistribution(new ConstantDistribution(0), d, MINUS);
            }
        }
        if (c == '(') {
            state.pos++;
            Distribution result = parseExpression(state);
            parseCharacter(state, ')');
            return result;
        } else if (c == '[') {
            return parseDiscreteDistribution(state);
        } else if (Character.isDigit(c) || c == '.') {
            return new ConstantDistribution(parseConstant(state));
        } else if (c == 'm') {
            int pos = state.pos;
            boolean isMin;
            if (state.expr.startsWith("min", pos)) {
                isMin = true;
            } else if (state.expr.startsWith("max", pos)) {
                isMin = false;
            } else {
                raiseException(state, "\"min\" or \"max\" expected");
                return null;
            }
            state.pos += 3;
            return parseMinMax(state, isMin);
        } else if (c == 0) {
            raiseException(state, "more characters expected");
            return null;
        } else { // specific distribution
            return parseDistribution(state);
        }
    }

    private static Distribution parseMinMax(ParsingState state, boolean isMin) throws DistributionFormatException {
        parseCharacter(state, '(');
        Distribution result = parseExpression(state);
        while (state.curChar() == ',') {
            state.pos++;
            Distribution argument = parseExpression(state);
            result = new CompositeDistribution(result, argument, isMin ? MIN : MAX);
        }
        parseCharacter(state, ')');
        return result;
    }

    private static DiscreteDistribution parseDiscreteDistribution(ParsingState state) throws DistributionFormatException {
        ArrayList<Distribution> distributions = new ArrayList<Distribution>();
        ArrayList<Double> probabilities = new ArrayList<Double>();
        parseCharacter(state, '[');
        while (true) {
            distributions.add(parseExpression(state));
            parseCharacter(state, ':');
            int start = state.pos;
            double p = parseConstant(state);
            if (p <= 0) {
                state.pos = start;
                raiseException(state, "probability must be positive");
            }
            probabilities.add(p);
            if (state.curChar() == ']') {
                state.pos++;
                break;
            }
            parseCharacter(state, ',');
        }
        return new DiscreteDistribution(distributions, probabilities);
    }

    private static Distribution parseDistribution(ParsingState state) throws DistributionFormatException {
        char c = state.curChar();
        int paramsCount;
        switch (c) {
            case 'U':
            case 'N':
            case 'G':
                paramsCount = 2;
                break;
            case 'E':
                paramsCount = 1;
                break;
            default:
                raiseException(state, "unexpected character");
                return null;
        }
        int start = state.pos++;
        double[] params = parseDistributionParameters(state, paramsCount);
        try {
            switch (c) {
                case 'U':
                    return new UniformDistribution(params[0], params[1]);
                case 'N':
                    return new NormalDistribution(params[0], params[1]);
                case 'E':
                    return new ExponentialDistribution(params[0]);
                case 'G':
                    return new GammaDistribution(params[0], params[1]);
                default:
                    throw new AssertionError();
            }
        } catch (IllegalArgumentException e) {
            state.pos = start;
            raiseException(state, "illegal distribution parameters: " + e.getMessage());
            return null;
        }
    }

    private static double[] parseDistributionParameters(ParsingState state, int paramsCount) throws DistributionFormatException {
        parseCharacter(state, '(');
        double[] result = new double[paramsCount];
        for (int i = 0; i < paramsCount; i++) {
            result[i] = parseConstant(state);
            parseCharacter(state, i == paramsCount - 1 ? ')' : ',');
        }
        return result;
    }

    private static double parseConstant(ParsingState state) throws DistributionFormatException {
        char c = state.curChar();
        if (c != '-' && c != '.' && !Character.isDigit(c)) {
            raiseException(state, "digit, '-' or '.' expected");
        }
        int start = state.pos++;
        while (true) {
            c = state.curChar();
            if (!Character.isDigit(c) && c != '.') { // only simple decimal format is accepted
                break;
            }
            state.pos++;
        }
        try {
            return Double.parseDouble(state.expr.substring(start, state.pos));
        } catch (NumberFormatException e) {
            raiseException(state, "bad constant format");
            return Double.NaN; // just to make compiler happy
        }
    }

    private static void parseCharacter(ParsingState state, char c) throws DistributionFormatException {
        if (state.curChar() != c) {
            raiseException(state, "\'" + c + "\' expected");
        }
        state.pos++;
    }

    private static void raiseException(ParsingState state, String msg) throws DistributionFormatException {
        throw new DistributionFormatException(state.expr, state.pos, msg);
    }

    private static class ParsingState {
        int pos;
        final String expr;

        ParsingState(String expr) {
            this.expr = expr;
        }

        public char curChar() {
            return pos < expr.length() ? expr.charAt(pos) : 0;
        }
    }
}
